%include "boot.inc"
SECTION LOADER vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR

;--------- gdt描述符 ----------
; 第0描述符，无用
GDT_BASE:
    dd 0x00000000
    dd 0x00000000

; 第1描述符，代码段描述符
GDT_CODE:
    dd 0x0000FFFF
    dd DESC_CODE_HIGH

; 第2描述符, 数据,栈段描述符
GDT_DATA_STACK:
    dd 0x0000FFFF
    dd DESC_DATA_HIGH

; 第3描述符, 显存段描述符
GDT_VIDEO:
    dd 0x80000007 ; limit = (0xbffff - 0xb8000) / 4K = 0x7
    dd DESC_VIDEO_HIGH

    GDT_SIZE equ $ - GDT_BASE  
    GDT_LIMIT equ GDT_SIZE - 1

    times 120 dd 0 ; 填充60个8字节, 意为保留60个段描述符空位
    
;------- 定义段选择子 ---------
    SELECTOR_CODE equ (0x0001 << 3) + TI_GDT + RPL0
    SELECTOR_DATA equ (0x0002 << 3) + TI_GDT + RPL0
    SELECTOR_VIDEO equ (0x0003 << 3) + TI_GDT + RPL0

;------ 0x15中断检测出的内存容量数据储存位置 ------
    ; 此处距开头为0x200 = 60*8 + 4*8  = 64*8 = 512, 
    ; loader.bin会被加载内存0x900
    ; 所以total_mem_bytes在内存中地址是0xb00 = 0x200 + 0x900
    total_mem_bytes dd 0 ; 4 bytes, 用于储存检测出内存容量，单位字节

;------- 填写GDTR寄存器, 保存GDT表位置 --------
; GDTR寄存器0-15位: GDT界限
; 16-48位: GDT内存起始地址
    gdt_ptr dw GDT_LIMIT ; 低16位    
            dd GDT_BASE ; 高32位

;------ ards地址范围描述符结构--------
; 人工对齐，从total_mem_bytes开始：total_mem_bytes4 + gdt_ptr6 + args_buf244 + args_num2 = 256 = 0x100
    ards_buf times 244 db 0
    ards_num dw 0

;--------- loader代码-------
; 内存0xc00 =0xb00 + 0x100处, mbr.s加载完loader.bin后，可以直接跳到该处执行
loader_start:

;--------0x15中断0xe820, 获取所有ards结构----------
; int 0x15输入：eax=0xe820, ebx=0, ecx=20, edx=0x534d4150, di=ards_buf
    xor ebx, ebx
    mov edx, 0x534d4150
    mov di, ards_buf

.e820_get_mem_loop:
    mov eax, 0xe820
    mov ecx, 20
    int 0x15

    add di, cx ; 下一个ards结构地址位置
    inc word [ards_num] ; ards数量+1
    cmp ebx, 0 ; 如果ebx=0, 说明内存已经检测完，这是最后一个，已经没有ards结构
    jnz .e820_get_mem_loop

;---------在所有ards结构中找到[base_add_low+lenght_low]最大值，即内存容量----
    mov cx, [ards_num]
    mov ebx, ards_buf ; ebx 中储存ards开始地址
    xor edx, edx ; edx要中储存[base_add_low+lenght_low]最大值. 现在置0
.find_max_mem_area:
    mov eax, [ebx] ; ebx为base_add_low位置
    add eax, [ebx+8] ; ebx+8为lenght_low位置, eax为[base_add_low+lenght_low]值
    add ebx, 20 ; 下一个ards地址
    
    ; 如果eax>edx, 把eax里的值赋给ebx; 如果eax<=edx, 进入.next_ards 
    cmp edx, eax  
    jge .next_ards
    mov edx, eax 

.next_ards:
    loop .find_max_mem_area

    mov [total_mem_bytes], edx

; 进入保护模式
; -------- 1, 打开A20Gate, 禁止地址回绕
    in al, 0x92
    or al, 00000010b
    out 0x92, al
; -------- 2, 把GDT表基地址和长度加载GDTR寄存器, 
    lgdt [gdt_ptr]
; -------- 3, cr0 第0位置1
    mov eax, cr0
    or eax, 0x00000001
    mov cr0, eax
    
    jmp dword SELECTOR_CODE:p_mode_start ; 刷新流水线 

[bits 32]
p_mode_start:
    mov ax, SELECTOR_DATA
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov esp, LOADER_STACK_TOP
    mov ax, SELECTOR_VIDEO
    mov gs, ax

    mov byte [gs:160], 'P'
    mov byte [gs:161], 0x1f


;---------- 加载内核
    mov eax, KERNEL_START_SECTOR
    mov ebx, KERNEL_BIN_BASE_ADDR
    mov ecx, 200
    call rd_disk_m_32 ; eax, ebx, ecx为函数rd_disk_m_32的参数
                      ; rd_disk_m_32从指定扇区开始(eax=0x9)读取指定扇区数(ecx=200), 加载到指定内存位置(ebx=0x70000)
                      ; 和mbr.s中rd_disk_m_16大致相同, 不过该函数针对32位的保护模式

; ---------  启动分页模式
; 1.准备好页表，创建页目录和页表
    call setup_page
    sgdt [gdt_ptr] ; 取出gdt寄存器值，保存在gdt_ptr
    mov ebx, [gdt_ptr+2] ; gdt基址位置保存在ebx
    or dword [ebx + 0x18 + 4], 0xc0000000 ; ebx + 0x18, 为显存段描述符位置, 显存段在第3个gdt，每个8字节
                                      ; 为24=0x18. 而+4为高四位值,给24-31bit 1字节赋值0xc0
                                      ; 内存4G, 要改变加载gdt表位置
    add dword [gdt_ptr + 2], 0xc0000000 ; 改变gdt_ptr内，gdt基址的位置
    add esp, 0xc0000000 ; 同理改变栈顶位置

; 把页目录表地址赋cr3
    mov eax, PAGE_DIR_TABLE_POS
    mov cr3, eax
; 打开cr0中31pg位, 分页模式打开
    mov eax, cr0
    or eax, 0x80000000
    mov cr0, eax
; 加载gdt
    lgdt [gdt_ptr]

; 表示已经打开分页模式
    mov byte [gs:320], 'V'
    mov byte [gs:321], 0x1f

; 初始化kernel
    jmp SELECTOR_CODE:enter_kernel

enter_kernel:
    call kernel_init
    mov esp, 0xc009f000 ; 0xc0000000开始对应物理地址开始1M
    jmp KERNEL_ENTRY_POINT

    jmp $

; ----------- 创建页目录以及页表 --------
setup_page:
;把页目录占用的4096字节空间置0
    mov ecx, 4096
    mov esi, 0
.clear_page_dir:
    mov byte [PAGE_DIR_TABLE_POS + esi], 0
    inc esi
    loop .clear_page_dir

;开始创建页目录表项PDE
.create_pde:
    mov eax, PAGE_DIR_TABLE_POS
    add eax, 0x1000 ; 0x1000=4*1024B页目录表大小
                    ; 现在eax里储存为页表内存中开始位置
    mov ebx, eax ; ebx保存PAGE_DIR_TABLE_POS+0x1000, 内存中页表开始地址

    or eax, PG_US_U|PG_RW_W|PG_P ; PG_US_U|PG_RW_W|PG_P=0x7为页目录表项中0-11位
                                 ; 原eax=0x100000 + 0x1000页表在内存地址, 
                                 ; 现在eax中存储一个页目录表项的值,其值指向页表开始
                                 ; 任何特权程序都可以访问, 该页面可读写执行, 有效
    
    mov [PAGE_DIR_TABLE_POS + 0x0], eax ; 第0个页目录表项
    mov [PAGE_DIR_TABLE_POS + 0xc00], eax  ; 0xc00/4=3072/4=768, 第768个目录表项,
                                           ; 0x0-0xc00页目录中的页表为用户空间
                                           ; 0xc00-0xfff页目录中的页表都用于内核空间
                                           ; 现在第0和第768目录表项都指向同一个地址为页表地址
                                           ; 768及以上是未来内核储存，而第0页目录表项也一样，
                                           ; loader还没完成它的任务，为了在分页模式下，loader也可已运行，
                                           ; loader保存在底端1M内存中
    sub eax, 0x1000 ; eax中为指向页目录表自身的页目录表项值
    mov [PAGE_DIR_TABLE_POS + 0xffc], eax ; 0xffc = 4092, 最后一个页目录表项地址, 表项指向页目录表自身
                                     ; 页目录表占一页4096内存大小，
    
; 创建页表项PTE
    mov ecx, 256 ; 底端物理内存1M/4096 = 256页, 内核加载在底端1M物理内存中
    mov esi, 0
    mov edx, PG_US_U|PG_RW_W|PG_P ; 0x7, 内存地址从0开始

.create_pte:
    mov [ebx+esi*4], edx ; ebx为页表项地址, edx为页表项的值，为所指内存地址[4KB大小]和属性
    add edx, 0x1000 ; edx+4096, 下一个页表指向内存的地址的页表项值
    inc esi
    loop .create_pte

; 创建内核页表的PDE 
    mov eax, PAGE_DIR_TABLE_POS
    add eax, 0x2000 ; 第1个页表的位置(第二个)
    or eax, PG_US_U|PG_RW_W|PG_P ; 第1个页表的页目录表项值
    mov ebx, PAGE_DIR_TABLE_POS
    mov ecx, 254          ; 769 - 1022所有目录项数量; 目录项从0开始，最后一个目录项值指向目录表
    mov esi, 769      ;
.create_kernel_pde:
    mov [ebx+esi*4], eax
    inc esi
    add eax, 0x1000
    loop .create_kernel_pde
    ret

;----------保护模式下读取磁盘函数     
rd_disk_m_32:
    mov esi, eax ; 备份eax
    mov di, cx ; 备份cx

; 读取硬盘7步
; 1. 设置要读取扇区数
    mov dx, 0x1f2 ; 启动磁盘通常都是第1通道primary通道下, 0x1f2端口，读写sector count寄存器
    mov al, cl ; 把要储存在cx寄存器中的读取扇区数，写入到ax寄存器中
    out dx, al ; 把al存储的sector count写入端口0x1f2，即写入sector count寄存器中

    mov eax, esi ; 恢复eax

; 2. 使用LBA28，将LBA地址低24位地址写入3个LBA寄存器中通过端口0x1f3, 0x1f4, 0x1f5. 
    mov dx, 0x1f3
    out dx, al

    mov cl, 8
    shr eax, cl
    mov dx, 0x1f4
    out dx, al

    shr eax, cl
    mov dx, 0x1f5
    out dx, al

; 3. 通过0x1f6端口，把24-27位写入device寄存器低4位，
; 第4位：指定主盘0和从盘1，第5,7位：固定1，第6位：LBA模式0和CHS模式1
   shr eax, cl 
   and al, 0x0f ; LBA扇区号24-27位，设置al低4位
   or al, 0xe0 ; 设置al高4位, 1110
   mov dx, 0x1f6
   out dx, al

; 4. 向0x1f7端口，写入读命令, 到command寄存器
    mov dx, 0x1f7
    mov al, 0x20 ; 0x20读操作
    out dx, al

; 5. 检测硬盘状态, 检测status寄存器
.not_ready:
    nop ; sleep一段时间，不打断之前的读操作. 因为都使用0x1f7端口
    in al, dx ; dx=0x1f7. 取出status寄存器中的值
    and al, 0x88 ; 0x88=1000 1000; 只保留第四位和第七位. 第4位为1表示已经数据准备好，第7位为1表示硬盘忙
    cmp al, 0x08 ; false说明第四位为0, 没有准备好
    jnz .not_ready

; 6. 硬盘准备好，可以从0x1f0端口读取数据了
    mov ax, di ; di要读取的扇区数
    mov dx, 256 ; data寄存器为2字节16bit, 一个扇区512字节. 读一个扇区256次
    mul dx ; ax*dx一共读多少次
    mov cx, ax
    mov dx, 0x1f0

.go_on_read:
    in ax, dx ; dx为0x1f0端口号，cx为循环次数 
    mov [ebx], ax ; 把读取数据写到[bx]=LOADER_BASE_ADDR
    add ebx, 2
    loop .go_on_read
    ret

;--------初始化内核函数
kernel_init:
    xor eax, eax
    xor ebx, ebx   ; ebx用来记录程序头表地址
    xor ecx, ecx   ; ecx用来记录程序头表中程序头数量
    xor edx, edx   ; edx用来记录程序头的大小

    mov dx, [KERNEL_BIN_BASE_ADDR+42] ; elf头中e_phentsize[程序头大小]
    mov ebx, [KERNEL_BIN_BASE_ADDR+28] ; elf头中e_phoff[程序头开始位置在文件中的偏移]
    add ebx, KERNEL_BIN_BASE_ADDR
    mov cx, [KERNEL_BIN_BASE_ADDR+44] ;  elf头中e_phnum[程序头表中程序头数量]

; 避免gcc版本问题，强制把起始的虚拟地址确定在0xc0001000
; 默认32位该值为0x08048000, 64位该值为0x400000
.each_segment:
   cmp byte [ebx+0], PT_LOAD  ; 如果p_type=PT_NULL, 该程序头未使用
   jne .NOTPTLOAD

   ; 为函数memcpy(dst, src, size)压入参数
   push dword [ebx+16] ; Elf32_Phdr中p_filesz值，为"segment"所占大小
                       ; 第三参数，大小
   mov eax, [ebx+4]    ; Elf32_Phdr中p_offset值，"segment"在文件中的偏移
   add eax, KERNEL_BIN_BASE_ADDR  ; segment在内存缓存区中虚拟地址
   push eax ; 第二参数，源地址

   mov eax, [ebx+8]  ; 用eax暂时保存[ebx+8]的值
   cmp eax, 0x08048000 ; 如果segment起始地址为0x08048000
   jne .elf_header      ; 改为0xc0001000作为第一参数压栈
   mov eax, 0xc0001000
.elf_header:
   push eax ; eax 保存[ebx+8]或0xc0001000 
            ; Elf32_Phdr中p_vaddr值，"segment"第一个字节在进程虚拟地址位置
            ; 第一参数，目的地址
   call mem_cpy ; 调用mem_cpy函数，完成段复制
   add esp, 12  ; 清理栈中三个参数

.NOTPTLOAD:
    add ebx, edx ; 跳到下一个程序头
    loop .each_segment
    ret

;-----------mem_cpy(dst, src, size)逐字节复制
mem_cpy:
    cld
    push ebp
    mov ebp, esp
    push ecx
    mov edi, [ebp+8] ; dst
    mov esi, [ebp+12] ; src
    mov ecx, [ebp+16]; size
    rep movsb
    ; 恢复环境
    pop ecx
    pop ebp
    ret

